package semant;

import symbol.ClassInfo;
import symbol.MethodInfo;
import symbol.Symbol;
import symbol.VarInfo;
import syntaxtree.And;
import syntaxtree.ArrayAssign;
import syntaxtree.ArrayLength;
import syntaxtree.ArrayLookup;
import syntaxtree.Assign;
import syntaxtree.Block;
import syntaxtree.BooleanType;
import syntaxtree.Call;
import syntaxtree.ClassDecl;
import syntaxtree.ClassDeclExtends;
import syntaxtree.ClassDeclSimple;
import syntaxtree.Equal;
import syntaxtree.Exp;
import syntaxtree.False;
import syntaxtree.Formal;
import syntaxtree.Identifier;
import syntaxtree.IdentifierExp;
import syntaxtree.IdentifierType;
import syntaxtree.If;
import syntaxtree.IntArrayType;
import syntaxtree.IntegerLiteral;
import syntaxtree.IntegerType;
import syntaxtree.LessThan;
import syntaxtree.MainClass;
import syntaxtree.MethodDecl;
import syntaxtree.Minus;
import syntaxtree.NewArray;
import syntaxtree.NewObject;
import syntaxtree.Not;
import syntaxtree.Plus;
import syntaxtree.Print;
import syntaxtree.Program;
import syntaxtree.Statement;
import syntaxtree.This;
import syntaxtree.Times;
import syntaxtree.True;
import syntaxtree.Type;
import syntaxtree.VarDecl;
import syntaxtree.While;
import util.List;
import visitor.TypeVisitor;
import errors.ErrorEchoer;

public class TableChecker implements TypeVisitor {

	Env env;
	ErrorEchoer err;
	ClassInfo currClass;
	MethodInfo currMethod;

	TableChecker(Env env, ErrorEchoer err) 
	{
		this.env = env;
		this.err = err;
		currClass = null;
		currMethod = null;
	}
	
	public Type visit(Program n)
	{
		/* Verifica o programa todo, todas as classes
		 */

		/* Avalia a funcao main */
		n.mainClass.accept(this);
		
		/* Visita lista de classes */
		for(List<ClassDecl> aux = n.classList; aux != null; aux = aux.tail) 
		{
			aux.head.accept(this);			
		}

		return new IntegerType(n.line, n.row);
	}

	public Type visit(MainClass n)
	{
		/* Precisa avaliar apenas o statement, antes disso prepara os objetos */
		currClass = env.classes.get(Symbol.symbol(n.className.s));
		currMethod = currClass.methods.get(Symbol.symbol("main"));

		/* Avalia apenas o statement que eh o que trabalha com tipos na classe */
		return n.s.accept(this);
	}

	public Type visit(ClassDeclSimple n) 
	{
		/* Precisa avaliar os metodos e atributos */
		currClass = env.classes.get(Symbol.symbol(n.name.s));

		for(List<VarDecl> aux = n.varList; aux != null; aux = aux.tail)
			aux.head.accept(this);

		for(List<MethodDecl> aux = n.methodList; aux != null; aux = aux.tail)
			aux.head.accept(this);

		return new IntegerType(n.line, n.row);
	}

	@Override
	public Type visit(ClassDeclExtends n) 
	{
		/* Precisa avaliar os metodos, atributos e a superclasse*/
		currClass = env.classes.get(Symbol.symbol(n.name.s));

		if(env.classes.get(Symbol.symbol(n.superClass.s)) == null)
			err.Error(n, new Object[] {"Não existe a superclasse '" + n.superClass.s + "'."});

		for(List<VarDecl> aux = n.varList; aux != null; aux = aux.tail)
			aux.head.accept(this);

		for(List<MethodDecl> aux = n.methodList; aux != null; aux = aux.tail)
			aux.head.accept(this);

		return new IntegerType(n.line, n.row);
	}

	
	public Type visit(VarDecl n) 
	{
		/* Verifica o tipo da variavel */
		if(n.type instanceof IdentifierType)
		{
			IdentifierType t = ((IdentifierType)n.type);

			if(env.classes.get(Symbol.symbol(t.name)) == null)	// Se o tipo nao existir
				err.Error(n, new Object[] {"'" + t.name + "' não é um tipo válido."});
		}

		return n.type;
	}
	
	public Type visit(MethodDecl n)
	{
		/* Verifica o metodo */
		currMethod = currClass.methods.get(Symbol.symbol(n.name.s));

		ClassInfo pai = currClass.base;

		if(pai != null) //Se existir classe pai
		{
			MethodInfo paiMethod = pai.methods.get(Symbol.symbol(n.name.s));

			if(paiMethod != null) // Se o metodo existir na classe pai
			{
				/* Verifica retornos */
				if(! paiMethod.type.isComparable(currMethod.type)) // Se retornos forem diferentes
				{
					err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Tipo de retorno '" + currMethod.type.toString() + "' difere do que foi declarado na classe pai '" + paiMethod.parent.toString() + "'. Deveria ser '" + paiMethod.type.toString() + "'."});
				}

				/* Verifica parâmetros */
				if(paiMethod.formals == null && currMethod.formals == null) //Ambos sem parâmetros
				{
					//OK!
				}
				else if(paiMethod.formals != null && currMethod.formals != null && paiMethod.formals.size() == currMethod.formals.size()) //Mesmo numero de parâmetros
				{
					// Confere cada um dos parametros
					for(List<VarInfo> paiVar = paiMethod.formals, currVar = currMethod.formals; paiVar != null && currVar != null; paiVar = paiVar.tail, currVar = currVar.tail)
					{
						if(! paiVar.head.type.isComparable(currVar.head.type))	// Se algum for diferente
						{
							err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Parâmetro de tipo '" + currVar.head.type.toString() + "' redefine o de tipo '" + paiVar.head.type.toString() + "' declarado na classe pai '" + pai.name.toString() + "'."});
							
							break;
						}
					}
				}
				else	// Numero diferente de parametros
				{
					if(paiMethod.formals == null || (currMethod.formals != null && currMethod.formals.size() > paiMethod.formals.size()))
						err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Número de parâmetros maior do que o definido na classe pai '" + pai.name.toString() + "'."});
					else if(currMethod.formals == null || (paiMethod.formals != null && currMethod.formals.size() < paiMethod.formals.size()))
						err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Número de parâmetros menor do que o definido na classe pai '" + pai.name.toString() + "'."});
					else
						err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Número de parâmetros diferente do que o definido na classe pai '" + pai.name.toString() + "'."});
				}
			}
		}

		for(List<Formal> aux = n.formals; aux != null; aux = aux.tail) 
			aux.head.accept(this);

		for(List<VarDecl> aux = n.locals; aux != null; aux = aux.tail) 
			aux.head.accept(this);

		for(List<Statement> aux = n.body; aux != null; aux = aux.tail) 
			aux.head.accept(this);

		// Verifica o retorno do metodo
		Type ret = n.returnExp.accept(this);

		// Se nao forem exatamente do mesmo tipo, confere a compatibilidade
		if(!(n.returnType.isComparable(ret)))
		{
			if(ret instanceof IdentifierType && n.returnType instanceof IdentifierType)	// Se ambos sao tipos objetos
			{
				ClassInfo c = env.classes.get(Symbol.symbol(((IdentifierType) ret).name));
				Boolean find = false;

				if(c == null) 	// Se nao foi encontrado descendentes em comum
				{
					err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Tipo '" + ret.toString() + "' retornado diferente da assinatura '" + n.returnType.toString() + "' do método."});
				}
				else			// Procurando...
				{
					c = c.base;

					while(c != null)	// Procurando...
					{
						ret = new IdentifierType(n.line,n.row,c.name.toString());

						if(ret.isComparable(n.returnType))	// Encontrou
						{
							find = true;
							break;
						}
						else								// Procurando...
						{
							c = c.base;
						}
					}
					if(!find)	// Se nao achou
					{
						err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Tipo '" + ret.toString() + " : " + n.returnExp.toString() + "' retornado diferente da assinatura '" + n.returnType.toString() + "' do método."});
					}
				}
			}
			else	// Tipos incompativeis
			{
				err.Error(n, new Object[] {"Método '" + currMethod.name.toString() + "' da classe '" + currClass.name.toString() + "' inválido. Tipo '" + ret.toString() + " : " + n.returnExp.toString() + "' retornado diferente da assinatura '" + n.returnType.toString() + "' do método."});
			}
		}

		return ret;
	}
	
	public Type visit(Formal n)
	{
		/* Verifica o tipo da variavel */
		if(n.type instanceof IdentifierType)
		{
			IdentifierType t = ((IdentifierType)n.type);

			// Se tipo nao existir
			if(env.classes.get(Symbol.symbol(t.name)) == null)
				err.Error(n, new Object[] {" Tipo '" + t.name + "' não é válido."});
		}

		return n.type = n.type;
	}
	
	public Type visit(IntArrayType n) 
	{
		return new IntArrayType(n.line,n.row);
	}
	
	public Type visit(BooleanType n)
	{
		return new BooleanType(n.line,n.row);
	}
	
	public Type visit(IntegerType n) 
	{
		return new IntegerType(n.line,n.row);
	}
	
	public Type visit(IdentifierType n) 
	{
		return new IdentifierType(n.line,n.row,n.name);
	}
	
	public Type visit(Block n) 
	{
		// Verifica os statements
		for(; n.body != null; n.body = n.body.tail)
			n.body.head.accept(this);

		return new IntegerType(n.line,n.row);
	}
	
	public Type visit(If n) 
	{
		// Verifica se a condicao existe e eh do if e do tipo boolean
		if(n.condition != null && n.condition.accept(this) instanceof BooleanType)
		{
			if(n.thenClause != null) n.thenClause.accept(this);
			if(n.elseClause != null) n.elseClause.accept(this);
		}
		else
		{
			err.Error(n, new Object[]{"Condição errada no 'if'. Deveria ser 'Boolean'."});
		}

		return new IntegerType(n.line,n.row);
	}
	
	public Type visit(While n) 
	{
		if(n.condition == null || !(n.condition.accept(this) instanceof BooleanType))
			err.Error(n, new Object[]{"Condição errada no 'while'. Deveria ser 'Boolean'."});
		else
			if(n.body != null) n.body.accept(this);

		return new IntegerType(n.line,n.row);
	}
	
	public Type visit(Print n) 
	{
		return n.exp.accept(this);
	}
	
	public Type visit(Assign n) 
	{
		Symbol name = Symbol.symbol(n.var.s);
		VarInfo var = null;

		if(currMethod != null) // Procura variavel
		{
			if(var == null) var = currMethod.localsTable.get(name);
			if(var == null) var = currMethod.formalsTable.get(name);
			if(var == null) var = currClass.attributes.get(name);
			if(var == null) err.Error(n, new Object[]{"Variável '" + n.var.s + "' não declarada."});
		}
		else
		{
			if(var == null) var = currClass.attributes.get(name);
			if(var == null) err.Error(n, new Object[]{"Variável '" + n.var.s + "' não declarada."});
		}

		Type exp = n.exp.accept(this);

		// Se forem de tipos diferentes
		if(var != null && !var.type.isComparable(exp))
			err.Error(n, new Object[]{"Atribuição inválida: '" + var.type.toString() + "' e '" + exp.toString() + "' são incompatíveis."});

		return new IntegerType(n.line,n.row);
	}

	public Type visit(ArrayAssign n) 
	{
		if(!(n.var.accept(this) instanceof IntArrayType))		// 'n' nao e um vetor
			err.Error(n, new Object[]{"Atribuição inválida: '" + n.var.s + "' não é um array."});
		else if(!(n.index.accept(this) instanceof IntegerType))	// Indice indicado nao e inteiro
			err.Error(n, new Object[]{"Índice inválido: '" + n.index.toString() + "' não é um inteiro"});
		else if(!(n.value.accept(this) instanceof IntegerType))	// Valor atribuido nao e inteiro
			err.Error(n, new Object[]{"Atribuição inválida: '" + n.value.toString() + "' não é um inteiro."});

		return new IntegerType(n.line, n.row);
	}
	
	public Type visit(And n) 
	{
		if(!(n.lhs.accept(this) instanceof BooleanType))
			err.Error(n, new Object[]{"Tipo incorreto no lado esquerdo da expressão: '" + n.lhs.type.toString() + "'. Deveria ser 'Boolean'."});
		if(!(n.rhs.accept(this) instanceof BooleanType))
			err.Error(n, new Object[]{"Tipo incorreto no lado direito da expressão: '" + n.rhs.type.toString() + "'. Deveria ser 'Boolean'."});

		return n.type = new BooleanType(n.line, n.row);
	}
	
	public Type visit(LessThan n) 
	{
		if(!(n.lhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado esquerdo da expressão: '" + n.lhs.type.toString() + "'. Deveria ser 'Int'."});
		if(!(n.rhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado direito da expressão: '" + n.rhs.type.toString() + "'. Deveria ser 'Int'."});

		return n.type = new BooleanType(n.line,n.row);
	}
	
	public Type visit(Plus n) 
	{
		if(!(n.lhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado esquerdo da expressão: '" + n.lhs.type.toString() + "'. Deveria ser 'Int'."});
		if(!(n.rhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado direito da expressão: '" + n.rhs.type.toString() + "'. Deveria ser 'Int'."});

		return n.type = new IntegerType(n.line,n.row);
	}
	
	public Type visit(Minus n) 
	{
		if(!(n.lhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado esquerdo da expressão: '" + n.lhs.type.toString() + "'. Deveria ser 'Int'."});
		if(!(n.rhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado direito da expressão: '" + n.rhs.type.toString() + "'. Deveria ser 'Int'."});

		return n.type = new IntegerType(n.line,n.row);
	}
	
	public Type visit(Times n) 
	{
		if(!(n.lhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado esquerdo da expressão: '" + n.lhs.type.toString() + "'. Deveria ser 'Int'."});
		if(!(n.rhs.accept(this) instanceof IntegerType))
			err.Error(n, new Object[]{"Tipo incorreto no lado direito da expressão: '" + n.rhs.type.toString() + "'. Deveria ser 'Int'."});

		return n.type = new IntegerType(n.line,n.row);
	}
	
	public Type visit(ArrayLookup n) 
	{
		Type a = n.array.accept(this);

		if(a instanceof IntArrayType)
		{
			Type i = n.index.accept(this);
			
			if(!(i instanceof IntegerType))
				err.Error(n, new Object[]{"Índice '" + i.toString() + "' inválido: Não é um inteiro."});
		}
		else
		{
			err.Error(n, new Object[]{"Variável '" + a.toString() + "' inválida: Não é um array."});
		}

		return n.type = new IntegerType(n.line,n.row);
	}
	
	public Type visit(ArrayLength n)
	{
		if(!(n.array.accept(this) instanceof IntArrayType))
			err.Error(n, new Object[]{"'" + n.array.toString() + "' não é um array. Não existe o campo 'length'."});

		return n.type = new IntegerType(n.line,n.row);
	}
	
	public Type visit(Call n) 
	{
		Type t = n.object.accept(this);

		MethodInfo metodo = null;
		ClassInfo classe = null;

		// Verifica se 't' eh um objeto e possua variaveis ou metodos que possam ser chamados
		if(t instanceof IdentifierType) 
		{
			classe = env.classes.get(Symbol.symbol(((IdentifierType) t).name));

			// Se a classe do objeto nao foi declarada
			if(classe == null)
			{
				err.Error(n, new Object[]{"Classe de objeto não declarado"}); //Classe de objeto nao existe
			}
			else 
			{
				metodo = classe.methods.get(Symbol.symbol(n.method.s));

				// Se o metodo chamado nao exite na classe em questao
				if(metodo == null)
				{
					err.Error(n, new Object[]{"Método chamado é inválido."}); //Metodo nao existe na classe
				}
				else
				{
					List<Exp> paramPass = n.actuals;
					List<VarInfo> paramRec = metodo.formals;
					String methodName = Symbol.symbol(n.method.s).toString();

					if(paramRec == null || paramPass == null)
					{
						if(paramPass != null) err.Error(n, new Object[]{"Número excessivo de parâmetros na chamada do método '" + methodName + "'."});
						else if (paramRec != null) err.Error(n, new Object[]{"Número insuficiente de parâmetros na chamada do método '" + methodName + "'."});
					}
					else if(paramPass.size() > paramRec.size()) 
					{
						err.Error(n, new Object[]{"Número excessivo de parâmetros na chamada do método '" + methodName + "'."});
					}
					else if(paramPass.size() < paramRec.size()) 
					{
						err.Error(n, new Object[]{"Número insuficiente de parâmetros na chamada do método '" + methodName + "'."});
					}
					else
					{
						for(; paramPass != null && paramRec != null; paramPass = paramPass.tail, paramRec = paramRec.tail) 
						{
							Type p = paramPass.head.accept(this);

							if(!p.isComparable(paramRec.head.type)) 
							{
								if(p instanceof IdentifierType) 
								{
									Symbol par = Symbol.symbol(((IdentifierType) p).name);
									ClassInfo c = env.classes.get(par);
									Boolean find = false;

									if(c == null) err.Error(n, new Object[]{"Tipo '" + ((IdentifierType) p).name + "' não válido."});
									else c = c.base;

									while(c != null) 
									{
										p = new IdentifierType(n.line, n.row, c.name.toString());
										if(p.isComparable(paramRec.head.type)) 
										{
											find = true;
											break;
										}
										else c = c.base;
									}
									if(!find) err.Error(n, new Object[]{"Parâmetro '" + p.toString() + ": " + paramPass.head.toString() + "' não é válido em '" + metodo.name.toString() + "'."});
								}
								else err.Error(n, new Object[]{"Parâmetro '" + p.toString() + ": " + paramPass.head.toString() + "' não é válido em '" + metodo.name.toString() + "'. Espera-se que seja do tipo '" + paramRec.head.type.toString() + "'."});
							}
						}
					}
				}
			}
		}
		else 
		{
			err.Error(n, new Object[]{"Chamada inválida: Tipo '" + t.toString() + "' não possui nenhum método válido."});
		}

		if(metodo == null) return n.type = new IntegerType(n.line, n.row);
		else return n.type = metodo.type;
	}

	public Type visit(IntegerLiteral n) 
	{
		return n.type =  new IntegerType(n.line, n.row);
	}
	
	public Type visit(True n)
	{
		return n.type =  new BooleanType(n.line, n.row);
	}

	public Type visit(False n) 
	{
		return n.type = new BooleanType(n.line, n.row);
	}
	
	public Type visit(IdentifierExp n) 
	{
		return n.type = n.name.accept(this);
	}
	
	public Type visit(This n)
	{
		if(currMethod.name.toString() == "main")
			err.Error(n, new Object[] {"'this' não pode ser usado em métodos estáticos!"});

		return n.type = new IdentifierType(n.line, n.row, currClass.name.toString());
	}

	public Type visit(NewArray n) 
	{
		Type t = n.size.accept(this);
		
		if(!(t instanceof IntegerType)) 
			err.Error(n, new Object[]{"Tamanho '" + t.toString() + "' de vetor inválido:  Não é um inteiro."});

		return n.type =  new IntArrayType(n.line,n.row);
	}

	
	public Type visit(NewObject n) 
	{
		if(env.classes.get(Symbol.symbol(n.className.s)) == null) 
			err.Error(n, new Object[]{"Classe '" + n.className.s + "' não foi definida."});

		return n.type = new IdentifierType(n.line, n.row, n.className.toString());
	}

	
	public Type visit(Not n)
	{
		if(!(n.exp.accept(this) instanceof BooleanType))
			err.Error(n, new Object[]{"Tipo incorreto: '" + n.exp.type.toString() + "'. Deveria ser 'Boolean'."});

		return n.type = new BooleanType(n.line, n.row);
	}

	
	public Type visit(Equal n)
	{
		if(n.lhs.accept(this).isComparable(n.rhs.accept(this)))
			err.Error(n, new Object[]{"Tipos diferentes. '" + n.lhs.type.toString() + "' é inconpatível com '" + n.rhs.type.toString() + "'."});

		return n.type = new BooleanType(n.line,n.row);
	}

	public Type visit(Identifier n) 
	{
		Symbol name = Symbol.symbol(n.s);
		VarInfo var = null;

		if(currMethod != null)
		{
			if(var == null) var = currMethod.localsTable.get(name);
			if(var == null) var = currMethod.formalsTable.get(name);
			if(var == null) var = currClass.attributes.get(name);
			if(var == null) err.Error(n, new Object[]{"Identificador '" + n.s + "' não existe."});
		}
		else
		{
			if(var == null) var = currClass.attributes.get(name);
			if(var == null) err.Error(n, new Object[]{"Identificador '" + n.s + "' não existe."});
		}

		if(var == null) return new IdentifierType(n.line, n.row, n.s);
		else return var.type;
	}

}
